package tkaczyk.simtalk.messenger;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.CharBuffer;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Logger;

import tkaczyk.simtalk.message.Message;
import tkaczyk.simtalk.message.MessageParser;
import tkaczyk.simtalk.message.MessagePreparer;
import tkaczyk.simtalk.messenger.StatusDisplay.Status;

public class Messenger {
	public static final int MAX_MESSAGE_LEN = 4096;

	private static int listenerThreadCount = 0;
	private static int receiverThreadCount = 0;
	private static int senderThreadCount = 0;

	private Thread listener;
	private Thread sender;
	private Thread receiver;

	private boolean connected = false;
	private Lock socketLock = new ReentrantLock();

	private ServerSocket listeningSocket;
	private Socket messagingSocket;

	private List<Message> sendBuffer = new Vector<Message>();
	private StatusDisplay statusDisplay = null;
	private MessageDisplay messageDisplay = null;
	private MessagePreparer msgPrep = new MessagePreparer();

	private static Logger log = Logger.getRootLogger();

	public class ListenerThread extends Thread {
		public void run() {
			setName("listener-" + listenerThreadCount++);
			log.debug("Starting thread: " + getName());
			try {
				updateStatus(Status.LISTENING, "Listening on: " + listeningSocket.getLocalPort());
				log.info("Listening on: " + listeningSocket.getLocalPort());
				
				messagingSocket = listeningSocket.accept();
				
				log.info("New connection accepted from " + messagingSocket.getInetAddress().toString() + ":"
						+ messagingSocket.getPort());
				updateStatus(Status.CONNECTED, "Connected to: " + messagingSocket.getInetAddress().toString() + ":"
						+ messagingSocket.getPort());
				connected = true;
				receiver = new ReceiverThread();
				sender = new SenderThread();
				receiver.start();
				sender.start();
			} catch (IOException e) {
				log.error("Exception while listening for connections.", e);
				updateStatus(Status.DISCONNECTED, "Error occurred. See log");
			}
		}
	}

	public class ReceiverThread extends Thread {
		private CharBuffer buffer = CharBuffer.allocate(MAX_MESSAGE_LEN);
		private StringBuilder received = new StringBuilder();
		private int length;
		private MessageParser msgParser = new MessageParser();

		public void run() {
			setName("receiver-" + receiverThreadCount++);
			log.debug("Starting thread: " + getName());
			try {
				BufferedReader incoming = new BufferedReader(new InputStreamReader(messagingSocket.getInputStream()));

				while (connected) {
					do {
						if (!incoming.ready())
							break;

						socketLock.lock();
						log.debug("Reading from socket.");
						length = incoming.read(buffer);
						log.trace("Incoming ready?: " + Boolean.toString(incoming.ready()));
						socketLock.unlock();
						buffer.rewind();

						received.append(buffer.subSequence(0, length));
						if (incoming.ready()) {
							continue;
						}
						if (received.length() > 0) {
							log.debug("Read: \"" + received.toString() + "\"");
							msgParser.parseXML(received.toString());
							for (Message message : msgParser) {
								if (messageDisplay != null) {
									messageDisplay.update(message);
								}
							}
						}
						received.delete(0, received.length());
					} while (length > 0);
					sleep(20);
				}
			} catch (InterruptedException e) {
				log.error("Exception while receiving messages.", e);
				updateStatus(Status.DISCONNECTED, "Error occurred. See log");
			} catch (IOException e) {
				log.error("Exception while receiving messages.", e);
				updateStatus(Status.DISCONNECTED, "Error occurred. See log");
			}
		}
	}

	public class SenderThread extends Thread {
		public void run() {
			setName("sender-" + senderThreadCount++);
			log.debug("Starting thread: " + getName());
			try {
				PrintWriter outgoing = new PrintWriter(messagingSocket.getOutputStream(), false);

				while (connected) {
					while (!sendBuffer.isEmpty()) {
						Message msg = sendBuffer.remove(0);
						socketLock.lock();
						log.debug("Writing to socket: \"" + msg.toXML() + "\"");
						outgoing.print(msg.toXML());
						outgoing.flush();
						socketLock.unlock();
						if (messageDisplay != null) {
							messageDisplay.update(msg);
						}
					}
					sleep(20);
				}
			} catch (IOException e) {
				log.error("Exception while receiving messages.", e);
				updateStatus(Status.DISCONNECTED, "Error occurred. See log");
			} catch (InterruptedException e) {
				log.error("Exception while receiving messages.", e);
				updateStatus(Status.DISCONNECTED, "Error occurred. See log");
			}
		}
	}

	public Messenger() {
	}

	public void listen(int port) {
		log.debug("Attempting to listen on port " + port + ".");
		try {
			if (listeningSocket != null && !listeningSocket.isClosed()) {
				listeningSocket.close();
			}
			listeningSocket = new ServerSocket();
			listeningSocket.bind(new InetSocketAddress(port));
			listener = new ListenerThread();
			listener.start();
		} catch (IOException e) {
			log.error("Exception while attempting to listen.", e);
			updateStatus(Status.DISCONNECTED, "Error occurred. See log");
		}
	}

	public void connect(String remoteHost, int remotePort, int localPort) {
		try {
			connect(InetAddress.getByName(remoteHost), remotePort, localPort);
		} catch (UnknownHostException e) {
			log.error(e);
		}
	}
	
	public void connect(InetAddress remoteAddress, int remotePort, int localPort) {
		log.debug("Attempting to connect to " + remoteAddress.toString() + ":" + remotePort + ".");
		try {
			if (listeningSocket != null && !listeningSocket.isClosed()) {
				listeningSocket.close();
			}
			messagingSocket = new Socket();
			messagingSocket.bind(new InetSocketAddress(localPort));
			messagingSocket.connect(new InetSocketAddress(remoteAddress, remotePort), 1000);
			log.info("Connected to " + remoteAddress.toString() + ":" + remotePort + ".");
			updateStatus(Status.CONNECTED, "Connected to " + remoteAddress.toString() + ":" + remotePort + ".");
			connected = true;
			receiver = new ReceiverThread();
			sender = new SenderThread();
			receiver.start();
			sender.start();
		} catch (IOException e) {
			log.error("Exception while trying to connect.", e);
			updateStatus(Status.DISCONNECTED, "Error occurred. See log");
			connected = false;
		}
	}

	public void disconnect(int port) {
		log.debug("Attempting to disconnect.");
		try {
			connected = false;
			if (receiver != null)
				receiver.join();
			if (sender != null)
				sender.join();
			if (messagingSocket != null && messagingSocket.isConnected()) {
				messagingSocket.close();
				log.info("Disconnected.");
				updateStatus(Status.DISCONNECTED, "Disconnected");
			}
			if (port != 0) {
				listen(port);
			}
		} catch (InterruptedException e) {
			log.error("Exception while trying to disconnect.", e);
			updateStatus(Status.DISCONNECTED, "Error occurred. See log");
		} catch (IOException e) {
			log.error("Exception while trying to disconnect.", e);
			updateStatus(Status.DISCONNECTED, "Error occurred. See log");
		}
	}

	public void close() {
		log.info("Closing Messenger.");
		try {
			disconnect(0);
			if (listeningSocket != null && !listeningSocket.isClosed()) {
				listeningSocket.close();
			}
		} catch (IOException e) {
			log.error("IOException while closing Messenger.", e);
			updateStatus(Status.DISCONNECTED, "Error occurred. See log");
		}
	}

	public void SendMessage(Message msg) {
		sendBuffer.add(msg);
	}

	public Message SendMessage(String body) {
		Message msg = msgPrep.newMessage(body);
		SendMessage(msg);
		return msg;
	}

	public void registerStatusDisplay(StatusDisplay statusDisplay) {
		this.statusDisplay = statusDisplay;
	}

	public void setStatusDisplay(StatusDisplay statusDisplay) {
		registerStatusDisplay(statusDisplay);
	}
	
	private void updateStatus(StatusDisplay.Status status, String statusMsg) {
		if (statusDisplay != null) {
			statusDisplay.update(status, statusMsg);
		}
	}

	public void registerMessageDisplay(MessageDisplay messageDisplay) {
		this.messageDisplay = messageDisplay;
	}

	public void setMessageDisplay(MessageDisplay messageDisplay) {
		registerMessageDisplay(messageDisplay);
	}

	public void setNick(String nick) {
		msgPrep.setNick(nick);
	}
}